﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Diagnostics;
using Centny.PdfReader.Types;
using Centny.PdfReader.IO;
using System.IO.Compression;
using Centny.PdfReader.Tools;
using Centny.PdfReader.Security;
using Centny.PdfReader.Except;
namespace Centny.PdfReader.Pdf
{
    public class PdfDocument
    {
        public const String StartXref = "startxref";
        public static PdfDocument Open(Stream s, String pwd = null)
        {
            PdfDocument doc = new PdfDocument(s);
            doc.InitDocument(pwd);
            return doc;
        }
        //
        public Stream FileStream { get; set; }
        private PdfDictionary _Trailer;
        private PdfXref _Xref;
        private PdfLexer _Lexer;
        public PdfDictionary Trailer
        {
            get
            {
                return this._Trailer;
            }
        }
        public PdfXref Xref
        {
            get
            {
                return this._Xref;
            }
        }
        public PdfLexer Lexer
        {
            get
            {
                return this._Lexer;
            }
        }
        public PdfStandardSecurityHandler SecurityHandler { get; set; }
        public bool IsEncypted
        {
            get;
            set;
        }
        public PdfDocument(Stream fs)
        {
            this.FileStream = fs;
            this._Lexer = new PdfLexer(fs);
            this._Lexer.Doc = this;
            this._Xref = new PdfXref();
            this._Xref.Doc = this;
        }

        public void InitDocument(String pwd = null)
        {
            this._Lexer.Position = this._Lexer.Length - 150;
            this._Lexer.ToBlock(PdfDocument.StartXref);
            long xposition = this._Lexer.NextLongToken();
            this.ReadXref(xposition);
            PdfReference eref = this.Trailer[TrailerKeys.Encrypt] as PdfReference;
            this.IsEncypted = eref != null;
            if (this.IsEncypted)
            {
                this.SecurityHandler = new PdfStandardSecurityHandler(this.Encrypt() as PdfClassObject, this.Trailer);
                if ((!this.SecurityHandler.GenerateGlobalKey(pwd, true)) && (!this.SecurityHandler.GenerateGlobalKey(pwd)))
                {
                    throw new PdfOpenException("Invalid Password");
                }
            }
        }
        private PdfXref ReadXref(long position)
        {
            this._Lexer.Position = position;
            String xref = this._Lexer.NextToken();
            Debug.Assert("xref" == xref);
            int start = this._Lexer.NextIntToken();
            int count = this._Lexer.NextIntToken();
            int xreflen = start + count;
            Asserts.Assert(count > 0);
            this.Lexer.SkipWhite();
            for (int i = start; i < xreflen; i++)
            {
                String line = this.Lexer.ReadLine();
                String[] ts = line.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
                if (ts.Length < 3)
                {
                    continue;
                }
                PdfPointer p = new PdfPointer();
                p.Offset = long.Parse(ts[0]);
                p.MTime = int.Parse(ts[1]);
                p.State = ts[2][0];
                this.Xref.Xrefs[i] = p;
            }
            String token = this.Lexer.NextToken();
            Asserts.Assert("trailer".Equals(token));
            PdfDictionary tra = this.ReadTrailer();
            if (this._Trailer == null)
            {
                this._Trailer = tra;
            }
            PdfLong prev = tra[TrailerKeys.Prev] as PdfLong;
            if (prev != null)
            {
                this.ReadXref(prev.AsLong);
            }
            return this.Xref;
        }
        private PdfDictionary ReadTrailer()
        {
            PdfDictionary t = (PdfDictionary)this.Lexer.Next();
            Asserts.Assert(t != null, "Trailer is null");
            return t;
        }
        public PdfObject RealObject(PdfReference pref, PdfLexer.ObjectType type = PdfLexer.ObjectType.None)
        {
            if (this.Sobjs.ContainsKey(pref))
            {
                return this.Sobjs[pref];
            }
            if (!this.Xref.Xrefs.ContainsKey(pref.Num))
            {
                return null;
            }
            PdfPointer p = this.Xref.Xrefs[pref.Num];
            this.Lexer.Position = p.Offset;
            PdfObject pobj = this.Lexer.NextObject(type);
            return pobj;
        }
        private Dictionary<PdfReference, PdfClassObject> Sobjs = new Dictionary<PdfReference, PdfClassObject>();
        public void StoreObject(PdfReference pref, PdfClassObject cobj)
        {
            this.Sobjs[pref] = cobj;
        }
        public void RelObject(PdfReference pref)
        {
            if (this.Sobjs.ContainsKey(pref))
            {
                this.Sobjs.Remove(pref);
            }
        }
        public Stream ReadStream(PdfStreamObject so)
        {
            return this.ReadStream(so, true);
        }
        public Stream ReadStream(PdfStreamObject so, bool deflated)
        {
            PdfClass l = so[StreamKeys.Length];
            PdfLong len;
            if (l is PdfLong)
            {
                len = l as PdfLong;
            }
            else if (l is PdfReference)
            {
                PdfClassObject pco = this.RealObject(l as PdfReference) as PdfClassObject;
                len = pco.InnerObj as PdfLong;
            }
            else
            {
                len = null;
            }
            if (len == null)
            {
                throw new InvalidDataException("Unknow stream");
            }
            this.Lexer.Position = so.Stream.Position;
            byte[] data = this.Lexer.NextByte((int)len.Val);
            MemoryStream ms;
            if (this.IsEncypted)
            {
                byte[] objkey = this.SecurityHandler.GenerateObjectKey(so);
                byte[] undata = PdfStandardSecurityHandler.Rc4Decryption(data, objkey);
                ms = new MemoryStream(undata);
            }
            else
            {
                ms = new MemoryStream(data);
            }
            PdfName filter = so[StreamKeys.Filter] as PdfName;
            if (filter == null)
            {
                return ms;
            }
            if (!deflated)
            {
                return ms;
            }
            if ("/FlateDecode".Equals(filter.Name))
            {
                ms.ReadByte();
                ms.ReadByte();
                MemoryStream os = new MemoryStream();
                byte[] buf = new byte[1024];
                DeflateStream ds = new DeflateStream(ms, CompressionMode.Decompress);
                int rlen = 0;
                while ((rlen = ds.Read(buf, 0, 1024)) > 0)
                {
                    os.Write(buf, 0, rlen);
                }
                os.Position = 0;
                return os;
            }
            else
            {
                throw new NotImplementedException("not implemented Filter" + filter.Name);
            }
        }
        public PdfObject Encrypt()
        {
            return this.RealObject(this.Trailer[TrailerKeys.Encrypt] as PdfReference);
        }
        public PdfObject Catalog()
        {
            return this.RealObject(this.Trailer[TrailerKeys.Root] as PdfReference);
        }
        public PdfObject RootPages()
        {
            PdfClassObject cl = this.Catalog() as PdfClassObject;
            PdfDictionary dict = cl.InnerObj as PdfDictionary;
            Debug.Assert(dict != null);
            return this.RealObject(dict[CatalogKeys.Pages] as PdfReference);
        }
        private List<PdfObject> _AllPages = new List<PdfObject>();
        public List<PdfObject> AllPages
        {
            get
            {
                if (this._AllPages.Count > 0)
                {
                    return this._AllPages;
                }
                PdfObject rpage = this.RootPages();
                this.ListPages(this._AllPages, rpage);
                return this._AllPages;
            }
        }
        private void ListPages(List<PdfObject> pages, PdfObject p)
        {
            //System.Diagnostics.Debug.WriteLine(p.ToString());
            PdfClassObject parent = p as PdfClassObject;
            PdfDictionary el = parent.InnerObj as PdfDictionary;
            if (el == null)
            {
                return;
            }
            PdfName pn = el[PdfKeys.Type] as PdfName;
            if (pn == null)
            {
                return;
            }
            switch (pn.Name)
            {
                case "/Pages":
                    PdfArray ary = el[PagesKeys.Kids] as PdfArray;
                    foreach (PdfReference pref in ary.Objects)
                    {
                        PdfObject po = this.RealObject(pref);
                        this.ListPages(pages, po);
                    }
                    break;
                case "/Page":
                    pages.Add(p);
                    break;
                default:
                    break;
            }
        }
    }
}
